material-tableで日付や数値をセレクトリストでフィルタリングしてみる
こんにちは、CX事業本部の若槻です。
今回は、以前の記事で作成したReact + Material-UI + material-tableのアプリで、テーブルの各データの日付や数値の値をもとにセレクトリストでフィルタリングをできるようにしてみます。
実現したいこと
管理者が販売商品の一覧をテーブルビューで確認できるページで、テーブルの商品を以下の条件でセレクトリストによりフィルタリング表示できるようにしたいです。
- 予約のあり・なし(
予約数
が0
か1
以上か) - 予約期限の超過・未超過(
予約期限
が現在日を過ぎているか否か)
そこで上記の両フィルターをmaterial-tableのテーブルに対して実装してみます。
実装
セレクトリストの共通コンポーネント
まず今回実装したい両フィルターにて共通で使用するコンポーネントSelectList.tsx
をsrc/components/atoms
配下に作成します。
import React from 'react'; import MenuItem from '@material-ui/core/MenuItem'; import FormControl from '@material-ui/core/FormControl'; import Select from '@material-ui/core/Select'; const SelectList = (props: { columnDef: any; onFilterChanged: (rowId: string, filterValue: string) => void; items: [string, string][]; }) => { const { columnDef, onFilterChanged, items } = props; const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { onFilterChanged(columnDef.tableData.id, event.target.value as string); }; return ( <FormControl> <Select onChange={handleChange}> {items.map((item) => ( <MenuItem value={item[0]}>{item[1]}</MenuItem> ))} </Select> </FormControl> ); }; export default SelectList;
上記コードでは、SelectList
コンポーネントの引数としてmaterial-tableの仕様として渡されるcolumnDef
とonFilterChanged
に加えて、items
を[string, string][]
形式で渡し、
{items.map((item) => ( <MenuItem value={item[0]}>{item[1]}</MenuItem> ))}
上記箇所で<MenuItem>
をMapして作成することにより、セレクトリストのメニュー項目をコンポーネントの呼び出し側で動的に指定できるようにしています。
予約のあり・なしによるフィルタリング
src/components/pages/ProductPage.tsx
のコードを次のように変更します。
import React from 'react'; import MaterialTable from 'material-table'; import GenericTemplate from '../templates/GenericTemplate'; import { ArrowUpward } from '@material-ui/icons'; import * as colors from '@material-ui/core/colors'; import SelectList from '../atoms/SelectList'; const ProductPage: React.FC = () => { interface Product { itemName: string; price: number; reserveDuedate: string; reserveCount: number; } return ( <GenericTemplate title={'商品ページ'}> <MaterialTable icons={{ SortArrow: React.forwardRef((props, ref) => ( <ArrowUpward {...props} ref={ref} style={{ color: colors.blue[800] }} /> )), }} columns={[ { title: '商品名', field: 'itemName', defaultSort: 'asc', filtering: false, }, { title: '予約期限', field: 'reserveDuedate', filtering: false, }, { title: '価格', field: 'price', type: 'numeric', filtering: false, }, { title: '予約数', field: 'reserveCount', type: 'numeric', filterCellStyle: { textAlign: 'right' }, filterComponent: (props) => ( <SelectList columnDef={props.columnDef} onFilterChanged={props.onFilterChanged} items={[ ['all', 'すべて'], ['reserved', '予約あり'], ['notReserved', '予約なし'], ]} /> ), customFilterAndSearch: (filterValue: string, rowData: Product) => { if (filterValue === 'reserved') { return rowData.reserveCount > 0; } else if (filterValue === 'notReserved') { return rowData.reserveCount === 0; } return true; }, }, ]} data={[ { itemName: 'ペンライトセット', price: 20000, reserveDuedate: '2020/08/10', reserveCount: 20, }, { itemName: 'パンフレット', price: 4000, reserveDuedate: '2020/09/15', reserveCount: 0, }, { itemName: 'タオル', price: 3000, reserveDuedate: '2020/08/30', reserveCount: 5, }, { itemName: 'Tシャツ', price: 4500, reserveDuedate: '2020/08/30', reserveCount: 10, }, ]} options={{ showTitle: false, filtering: true, }} /> </GenericTemplate> ); }; export default ProductPage;
columnsのfilterComponent
プロパティにSelectList
コンポーネントを指定し、その引数items
にメニューの要素としてvalue
属性(all
、reserved
、notReserved
)と値(すべて
、予約あり
、予約なし
)の組の配列を渡して、予約数
列で使用するセレクトリストを作成しています。
filterComponent: (props) => ( <SelectList columnDef={props.columnDef} onFilterChanged={props.onFilterChanged} items={[ ['all', 'すべて'], ['reserved', '予約あり'], ['notReserved', '予約なし'], ]} /> ),
これにより下記のようなセレクトリストが予約数
列で使用可能となります。
そして、フィルター条件をcolumnsのcustomFilterAndSearch
プロパティにて定義します。セレクトリストで選択したメニューのvalue
属性の値(all
、reserved
、notReserved
のいずれか)がcustomFilterAndSearch
の第一引数、テーブルの各行ごとのデータが第二引数となるので、2つの引数をもとにフィルター条件を定義します。ここでtrue
が返された行のみがフィルター表示されます。
customFilterAndSearch: (filterValue: string, rowData: Product) => { if (filterValue === 'reserved') { return rowData.reserveCount > 0; } else if (filterValue === 'notReserved') { return rowData.reserveCount === 0; } return true; },
予約数
列にて予約のあり・なしによるフィルタリングが実装できました。操作時の画面は以下のようになります。
- フィルター適用前
-
セレクトリストで
予約あり
を選択 -
セレクトリストで
予約なし
を選択 -
セレクトリストで
すべて
を選択
予約期限の超過・未超過によるフィルタリング
日付の比較によるフィルタリングを可能とするためmoment
をインストールします。
% npm install moment --save
src/components/pages/ProductPage.tsx
のコードを次のように変更します。
ハイライトされた行が前項との変更部分(行追加)となります。
import React from 'react'; import MaterialTable from 'material-table'; import GenericTemplate from '../templates/GenericTemplate'; import { ArrowUpward } from '@material-ui/icons'; import * as colors from '@material-ui/core/colors'; import SelectList from '../atoms/SelectList'; import moment from 'moment'; const ProductPage: React.FC = () => { interface Product { itemName: string; price: number; reserveDuedate: string; reserveCount: number; } return ( <GenericTemplate title={'商品ページ'}> <MaterialTable icons={{ SortArrow: React.forwardRef((props, ref) => ( <ArrowUpward {...props} ref={ref} style={{ color: colors.blue[800] }} /> )), }} columns={[ { title: '商品名', field: 'itemName', defaultSort: 'asc', filtering: false, }, { title: '予約期限', field: 'reserveDuedate', filterComponent: (props) => ( <SelectList columnDef={props.columnDef} onFilterChanged={props.onFilterChanged} items={[ ['all', 'すべて'], ['notOverDue', '未超過'], ['overDue', '超過'], ]} /> ), customFilterAndSearch: (filterValue: string, rowData: Product) => { const jstNow = new Date().toLocaleString('ja', {}); if (filterValue === 'notOverDue') { return moment(rowData.reserveDuedate).isSameOrAfter(jstNow); } else if (filterValue === 'overDue') { return moment(rowData.reserveDuedate).isBefore(jstNow); } return true; }, }, { title: '価格', field: 'price', type: 'numeric', filtering: false, }, { title: '予約数', field: 'reserveCount', type: 'numeric', filterCellStyle: { textAlign: 'right' }, filterComponent: (props) => ( <SelectList columnDef={props.columnDef} onFilterChanged={props.onFilterChanged} items={[ ['all', 'すべて'], ['reserved', '予約あり'], ['notReserved', '予約なし'], ]} /> ), customFilterAndSearch: (filterValue: string, rowData: Product) => { if (filterValue === 'reserved') { return rowData.reserveCount > 0; } else if (filterValue === 'notReserved') { return rowData.reserveCount === 0; } return true; }, }, ]} data={[ { itemName: 'ペンライトセット', price: 20000, reserveDuedate: '2020/08/10', reserveCount: 20, }, { itemName: 'パンフレット', price: 4000, reserveDuedate: '2020/09/15', reserveCount: 0, }, { itemName: 'タオル', price: 3000, reserveDuedate: '2020/08/30', reserveCount: 5, }, { itemName: 'Tシャツ', price: 4500, reserveDuedate: '2020/08/30', reserveCount: 10, }, ]} options={{ showTitle: false, filtering: true, }} /> </GenericTemplate> ); }; export default ProductPage;
実装の仕方は前項の「予約のあり・なし」と同じです。
columnsのfilterComponent
プロパティにSelectList
コンポーネントを指定し、その引数items
にメニューの要素を渡して、予約期限
列で使用するセレクトリストを作成しています。
filterComponent: (props) => ( <SelectList columnDef={props.columnDef} onFilterChanged={props.onFilterChanged} items={[ ['all', 'すべて'], ['notOverDue', '未超過'], ['overDue', '超過'], ]} /> ),
これにより下記のようなセレクトリストが予約期限
列で使用可能となります。
そして、フィルター条件をcolumnsのcustomFilterAndSearch
プロパティにて定義します。セレクトリストの選択状態および、各行ごとに日本時間の現在時刻とreserveDuedate
のmoment
オブジェクトの比較によってフィルタリングをしています。
customFilterAndSearch: (filterValue: string, rowData: Product) => { const jstNow = new Date().toLocaleString('ja', {}); if (filterValue === 'notOverDue') { return moment(rowData.reserveDuedate).isSameOrAfter(jstNow); } else if (filterValue === 'overDue') { return moment(rowData.reserveDuedate).isBefore(jstNow); } return true; },
予約数
列にて予約のあり・なしによるフィルタリングが実装できました。操作時の画面は以下のようになります。(執筆時点の日付は2020/08/16です。)
- フィルター適用前
-
セレクトリストで
超過
を選択 -
セレクトリストで
未超過
を選択 -
セレクトリストで
すべて
を選択
おわりに
React + Material-UI + material-tableのアプリで、テーブルの各データの日付や数値の値をもとにセレクトリストによるフィルタリングを実装してみました。
JavaScriptではなくTypeScriptによる実装であるため、Reactやmaterial-table特有の型を明示的に定義する必要があるのがスパルタな感じで大変でしたが、それによりコンポーネントの動作や渡す値の理解が明瞭となり鍛えられますね。
参考
- material-table で日付範囲の指定によるフィルタリングがしたい | Developers.IO
- moment.jsで日付時刻操作を行うTips | ささきしぶろぐ
- Array.prototype.map() | MDN Web Docs
- Typescript Map Relation with Array objects | stackoverflow
以上